iT邦幫忙

2023 iThome 鐵人賽

DAY 29
1

https://ithelp.ithome.com.tw/upload/images/20231005/20119486MfpIiHuWWj.png

前言

接下來這一篇將會來介紹 JWT 的驗證,畢竟前面一直都沒提到,可是實戰上來講卻是非常重要的一環哩。

身份認證

為什麼要身份認證呢?前面的許多章節中,其實我們都沒有做身份認證的機制,舉凡 API、Discord、Chrome Extension 等等,都沒有做身份認證,那麼為什麼要做身份認證呢?

我們所開發的東西有些功能是僅限於特定的人才能使用,例如:管理員、VIP 等等,這時候我們就需要一個機制來做身份認證,那麼這時候就會提到 JWT(JSON Web Token)了。

而現今主流開發比較常見於前後端分離的架構,不再是傳統的 SSR(Server Side Rendering)的架構,前端大多都是透過後端所提供的 API 來取得資料,而這個取資料的過程就必須要被認證。

Note
早期開發時,使用者登入後,後端會將使用者的 UID 儲存在伺服器的記憶體中(Session),但因為現今主流開發事前後端分離的架構,因此這種方式已經不適用了,因此就有了 JWT。

JSON Web Token 是什麼?

剛才有提到 JWT 的全名是 JSON Web Token,是一個廣泛被使用來驗證與授權的標準,尤其是在網頁開發上很常被使用,而且也是一個開放的標準(RFC 7519)。

基本上 JWT 的結構分為三個部分:

  • HEADER(標頭):ALGORITHM & TOKEN TYPE
    • 用來說明這一組 Token 是用什麼演算法加密的,以及 Token 的類型
  • PAYLOAD(負載):DATA
    • 主要是用來存放一些資料,例如使用者的 ID、名字等等
  • VERIFY SIGNATURE(驗證簽名)
    • 這一部分是用來驗證 Token 是否為正確的 Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

很難看出其差異吧?如果我依據上面的結構來拆解的話,就會變成這樣:

HEADER.PAYLOAD.VERIFY SIGNATURE

搭配上方的 JWT 範例,就會變成這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  # HEADER(標頭)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # VERIFY SIGNATURE(驗證簽名)

JWT 原理

那麼 JWT 是如何實作出來的呢?剛才有提到 JWT 的結構分為三個部分,其實比較核心的部分是 PAYLOAD(負載)與 VERIFY SIGNATURE(驗證簽名),但我們還是針對 JWT 三個部分來做介紹,而這邊示範的範例會是前面所講的 JWT Token 範例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  # HEADER(標頭)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # VERIFY SIGNATURE(驗證簽名)

HEADER(標頭)

HEADER 主要組合會是兩個部分:

  • alg(Algorithm):加密演算法
  • typ(Type):Token 類型

而這兩個部分會被編碼成 Base64 字串,例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  # HEADER(標頭)

PAYLOAD(負載)

PAYLOAD(負載)的部分,其實就是一個 JSON 物件,例如:

{
  "username": "Ray", // 使用者名稱
  "email": "example.com", // 使用者信箱
  "iat": 1516239022 // 簽發(Issued At)時間
}

而這個 JSON 物件會被編碼成 Base64 字串,例如:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)

感覺 PAYLOAD 滿單純的吧?但實際上它是由三個部分組成的,分別是:

  • Registered Claim(註冊聲明)
  • Public Claim(公開聲明)
  • Private Claim(私有聲明)

而這三個部分都是選填的,但實戰上來講,通常都會使用 Public Claim(公開聲明)來存放一些資料,例如:使用者的 ID、名字等等。

比較常見的設定會是 Registered Claim 中的 iat(Issued At)與 exp(Expiration Time),而這兩個屬性可以用來驗證 Token 是否過期等。

當然,Registered Claim 中還有很多屬性,例如:

  • iss(Issuer):發行者
  • sub(Subject):主體
  • aud(Audience):對象
  • exp(Expiration Time):過期時間
  • nbf(Not Before):生效時間
  • iat(Issued At):簽發時間
  • jti(JWT ID):JWT ID

因此以上面前面的 JSON 物件來說,就會變成這樣:

{
  "username": "Ray", // 使用者名稱 => Public Claim(公開聲明)
  "email": "example.com", // 使用者信箱 => Public Claim(公開聲明)
  "iat": 1516239022 // 簽發(Issued At)時間 => Registered Claim(註冊聲明)
}

Note
要注意一下 Claim(聲明)的單字只有三個字母,因為 JWT 旨在簡潔與輕量化,因此 Claim(聲明)的單字只有三個字母。

VERIFY SIGNATURE(驗證簽名)

VERIFY SIGNATURE(驗證簽名)的部分,其實就是將 PAYLOAD(負載)與 HEADER(標頭)進行編碼,然後再進行加密,例如:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

那麼問題來了,JWT 是如何知道要用什麼演算法來加密呢?其實就是透過 HEADER(標頭)來取得,例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

如果今天是用 PS256 演算法來加密的話,就會變成這樣:

{
  "alg": "PS256",
  "typ": "JWT"
}

接著就會將 PAYLOAD(負載)與 HEADER(標頭)進行編碼,然後再進行加密,例如:

PS256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

而這個加密後的字串也會被編碼成 Base64 字串。

那 secret 是什麼呢?其實就是一個私鑰,我們在針對 JWT TOken 生成時,會需要使用一組私鑰,而這組私鑰會被用來加密,而當我們要驗證 JWT Token 時,就會需要使用這組私鑰來解密。

那麼另一個問題來了,我們生成的 JWT Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以把它丟到 jwt.io 這個網站上,然後你就會發現它會自動幫你解析,並且告訴你這個 Token 是用什麼演算法加密的,以及 Token 的類型,如下圖:

https://ithelp.ithome.com.tw/upload/images/20231005/20119486hsBa4gcUKH.png

好的,問題來了

「為什麼我們沒有提供私鑰(secret),但是它卻可以解析出來呢?」

因為 JWT Token 只是透過 Base64 編碼,而並沒有進行加密,因此你是可以自己寫一個 JWT Token 解析器,這邊我也簡單示範寫一個 JWT Token 解析器範例:

// Base64 解碼
const Base64decoded = (str) => {
  str = str.replace(/-/g, '+').replace(/_/g, '/');

  // 如果字串長度不是 4 的倍數,補上 "="
  while (str.length % 4 !== 0) {
    str += '=';
  }

  return window.atob(str);
}

// 解析 JWT Token
const jwtTokenEncoded = (str) => {
  if(!str) return console.log('請輸入 JWT Token');
  const [header, payload, signature] = str.split('.');

  const headerDecoded = JSON.parse(Base64decoded(header));
  const payloadDecoded = JSON.parse(Base64decoded(payload));

  return {
    header: headerDecoded,
    payload: payloadDecoded,
  }
}

jwtTokenEncoded('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')

接著你貼到瀏覽器上就可以看到解析結果了,如下圖:

https://ithelp.ithome.com.tw/upload/images/20231005/20119486AO7t8ozdfS.png

因此實際上來講 JWT Token 是用 Base64 編碼的,而且有一定的安全性問題,所以千萬不要把敏感資訊放在 JWT Token 中,例如:使用者的密碼、使用者的信用卡資訊等等。

JWT Token 核心驗證的方式主要在於剛剛提到的 secret(私鑰),因此就算我們解析出來了,但是沒有私鑰的話,也是無法驗證成功。

那麼透過這一篇,我相信你應該對於 JWT Token 有一定的了解了,接下來我們就來看看如何實作 JWT Token 吧!

實作 JWT Token 發放

JWT Token 在 Node.js 上使用非常的簡單,只需要安裝 jsonwebtoken 套件

npm install jsonwebtoken

用法也很簡單,只需要安裝 jsonwebtoken 套件並這樣寫就可以了:

const jwt = require('jsonwebtoken');

const token = jwt.sign({
  username,
  email: 'example.com',
}, 'secret', { expiresIn: 60 });

console.log(token); // 這邊就會印出 JWT Token

jwt.sign 會接受三個參數:

  • payload:PAYLOAD(負載)
  • secret:私鑰
  • options:選項,也就是 Registered Claim(註冊聲明)

jwt.sign 會回傳一個 JWT Token,這個 JWT Token 就是我們要發放給使用者的 Token。

往後前端需要請求某些需要權限或驗證的 API 時,就可以將這個 JWT Token 放在 Header 中,例如:

axios.get('https://example.com/api', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

接著後端只需要使用 jwt.verify 就可以驗證 JWT Token 是否正確,例如:

const jwt = require('jsonwebtoken');

const token = jwt.sign({
  username,
  email: 'example.com',
}, 'secret', { expiresIn: 60 });

const decoded = jwt.verify(token, 'secret');

console.log(decoded); // 這邊就會印出解析後的 JWT Token

是不是超簡單的呢?這邊我就不額外提供範例了,你可以再試著自己嘗試看看。

Note
Bearer 是一種 HTTP 認證方式,你可以參考 Authentication 這篇文章。

那麼這一篇也差不多了,我們下一篇見哩。


上一篇
Day28-Google Extension 與 Google Apps Script 蹦再一起
下一篇
Day30-旅途告一個段落
系列文
《Node.js 不負責系列:把前端人員當作後端來用,就算是前端也能嘗試寫的後端~原來 Node.js 可以做這麼多事~》31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言